Introduce cargo metadata subcommand
authorAleksey Kladov <aleksey.kladov@gmail.com>
Sat, 5 Dec 2015 00:22:54 +0000 (03:22 +0300)
committerAleksey Kladov <aleksey.kladov@gmail.com>
Mon, 25 Jan 2016 14:12:37 +0000 (17:12 +0300)
Most of the work was done by @dan-t in #1225 and by @winger in #1434

Fixes #2193

src/bin/cargo.rs
src/bin/metadata.rs [new file with mode: 0644]
src/cargo/ops/cargo_output_metadata.rs [new file with mode: 0644]
src/cargo/ops/mod.rs
tests/test_cargo_metadata.rs [new file with mode: 0644]
tests/tests.rs

index 62556279d59235bba76866501f4a50ddf7cd3f4c..cc095eb73e6e68a3fea9a56cfb511d8d05b422dd 100644 (file)
@@ -73,6 +73,7 @@ macro_rules! each_subcommand{
         $mac!(install);
         $mac!(locate_project);
         $mac!(login);
+        $mac!(metadata);
         $mac!(new);
         $mac!(owner);
         $mac!(package);
diff --git a/src/bin/metadata.rs b/src/bin/metadata.rs
new file mode 100644 (file)
index 0000000..41a4dbd
--- /dev/null
@@ -0,0 +1,65 @@
+extern crate cargo;
+extern crate docopt;
+extern crate rustc_serialize;
+extern crate toml;
+
+use std::path::PathBuf;
+
+use cargo::ops::{output_metadata, OutputTo, OutputMetadataOptions};
+use cargo::util::important_paths::find_root_manifest_for_wd;
+use cargo::util::{CliResult, CliError, Config};
+
+#[derive(RustcDecodable)]
+struct Options {
+    flag_features: Vec<String>,
+    flag_manifest_path: Option<String>,
+    flag_no_default_features: bool,
+    flag_output_format: String,
+    flag_output_path: Option<String>,
+    flag_verbose: bool,
+    flag_quiet: bool,
+    flag_color: Option<String>,
+}
+
+pub const USAGE: &'static str = "
+Output the resolved dependencies of a project, the concrete used versions
+including overrides, in machine-readable format.
+
+Usage:
+    cargo metadata [options]
+
+Options:
+    -h, --help               Print this message
+    -o, --output-path PATH   Path the output is written to, otherwise stdout is used
+    -f, --output-format FMT  Output format [default: toml]
+                             Valid values: toml, json
+    --features FEATURES      Space-separated list of features
+    --no-default-features    Do not include the `default` feature
+    --manifest-path PATH     Path to the manifest
+    -v, --verbose            Use verbose output
+    -q, --quiet              No output printed to stdout
+    --color WHEN             Coloring: auto, always, never
+";
+
+pub fn execute(options: Options, config: &Config) -> CliResult<Option<()>> {
+    try!(config.shell().set_verbosity(options.flag_verbose, options.flag_quiet));
+    try!(config.shell().set_color_config(options.flag_color.as_ref().map(|s| &s[..])));
+    let manifest = try!(find_root_manifest_for_wd(options.flag_manifest_path, config.cwd()));
+
+    let output_to = match options.flag_output_path {
+        Some(path) => OutputTo::File(PathBuf::from(path)),
+        None => OutputTo::StdOut
+    };
+
+    let options = OutputMetadataOptions {
+        features: options.flag_features,
+        manifest_path: &manifest,
+        no_default_features: options.flag_no_default_features,
+        output_format: options.flag_output_format,
+        output_to: output_to,
+    };
+
+    output_metadata(options, config)
+        .map(|_| None)
+        .map_err(|err| CliError::from_boxed(err, 101))
+}
diff --git a/src/cargo/ops/cargo_output_metadata.rs b/src/cargo/ops/cargo_output_metadata.rs
new file mode 100644 (file)
index 0000000..38f084a
--- /dev/null
@@ -0,0 +1,126 @@
+use std::ascii::AsciiExt;
+use std::collections::HashMap;
+use std::path::{Path, PathBuf};
+
+use core::resolver::Resolve;
+use core::{Source, Package};
+use ops;
+use rustc_serialize::json;
+use sources::PathSource;
+use toml;
+use util::config::Config;
+use util::{paths, CargoResult};
+
+
+/// Where the dependencies should be written to.
+pub enum OutputTo {
+    File(PathBuf),
+    StdOut,
+}
+
+pub struct OutputMetadataOptions<'a> {
+    pub features: Vec<String>,
+    pub output_format: String,
+    pub output_to: OutputTo,
+    pub manifest_path: &'a Path,
+    pub no_default_features: bool,
+}
+
+/// Loads the manifest, resolves the dependencies of the project to the concrete
+/// used versions - considering overrides - and writes all dependencies in a TOML
+/// format to stdout or the specified file.
+///
+/// The TOML format is e.g.:
+/// ```toml
+/// root = "libA"
+///
+/// [packages.libA]
+/// dependencies = ["libB"]
+/// path = "/home/user/.cargo/registry/src/github.com-1ecc6299db9ec823/libA-0.1"
+/// version = "0.1"
+///
+/// [packages.libB]
+/// dependencies = []
+/// path = "/home/user/.cargo/registry/src/github.com-1ecc6299db9ec823/libB-0.4"
+/// version = "0.4"
+///
+/// [packages.libB.features]
+/// featureA = ["featureB"]
+/// featureB = []
+/// ```
+pub fn output_metadata(opt: OutputMetadataOptions, config: &Config) -> CargoResult<()> {
+    let deps = try!(resolve_dependencies(opt.manifest_path,
+                                         config,
+                                         opt.features,
+                                         opt.no_default_features));
+    let (resolved_deps, packages) = deps;
+
+    #[derive(RustcEncodable)]
+    struct RootPackageInfo<'a> {
+        name: &'a str,
+        version: String,
+        features: Option<&'a HashMap<String, Vec<String>>>,
+    }
+
+    #[derive(RustcEncodable)]
+    struct ExportInfo<'a> {
+        root: RootPackageInfo<'a>,
+        packages: Vec<&'a Package>,
+    }
+
+    let mut output = ExportInfo {
+        root: RootPackageInfo {
+            name: resolved_deps.root().name(),
+            version: format!("{}", resolved_deps.root().version()),
+            features: None,
+        },
+        packages: Vec::new(),
+    };
+
+    for package in packages.iter() {
+        output.packages.push(&package);
+        if package.package_id() == resolved_deps.root() {
+            let features = package.manifest().summary().features();
+            if !features.is_empty() {
+                output.root.features = Some(features);
+            }
+        }
+    }
+
+    let serialized_str = match &opt.output_format.to_ascii_uppercase()[..] {
+        "TOML" => toml::encode_str(&output),
+        "JSON" => try!(json::encode(&output)),
+        _ => bail!("unknown format: {}, supported formats are TOML, JSON.",
+                   opt.output_format),
+    };
+
+    match opt.output_to {
+        OutputTo::StdOut => println!("{}", serialized_str),
+        OutputTo::File(ref path) => try!(paths::write(path, serialized_str.as_bytes()))
+    }
+
+    Ok(())
+}
+
+/// Loads the manifest and resolves the dependencies of the project to the
+/// concrete used versions. Afterwards available overrides of dependencies are applied.
+fn resolve_dependencies(manifest: &Path,
+                        config: &Config,
+                        features: Vec<String>,
+                        no_default_features: bool)
+                        -> CargoResult<(Resolve, Vec<Package>)> {
+    let mut source = try!(PathSource::for_path(manifest.parent().unwrap(), config));
+    try!(source.update());
+
+    let package = try!(source.root_package());
+
+    let deps = try!(ops::resolve_dependencies(&package,
+                                              config,
+                                              Some(Box::new(source)),
+                                              features,
+                                              no_default_features));
+
+    let (packages, resolve_with_overrides, _) = deps;
+
+    Ok((resolve_with_overrides, packages))
+}
index 806d3921a6e7913a5f7a615bc5522bd15bebcc1f..b5c1efec65aa47695f72260134ece63c938b0c7e 100644 (file)
@@ -23,6 +23,7 @@ pub use self::registry::{modify_owners, yank, OwnersOptions};
 pub use self::cargo_fetch::{fetch, get_resolved_packages};
 pub use self::cargo_pkgid::pkgid;
 pub use self::resolve::{resolve_pkg, resolve_with_previous};
+pub use self::cargo_output_metadata::{output_metadata, OutputTo, OutputMetadataOptions};
 
 mod cargo_clean;
 mod cargo_compile;
@@ -31,6 +32,7 @@ mod cargo_fetch;
 mod cargo_generate_lockfile;
 mod cargo_install;
 mod cargo_new;
+mod cargo_output_metadata;
 mod cargo_package;
 mod cargo_pkgid;
 mod cargo_read_manifest;
diff --git a/tests/test_cargo_metadata.rs b/tests/test_cargo_metadata.rs
new file mode 100644 (file)
index 0000000..f491737
--- /dev/null
@@ -0,0 +1,107 @@
+use std::fs::File;
+use std::io::prelude::*;
+
+use hamcrest::{assert_that, existing_file, is, equal_to};
+use support::{project, execs, basic_bin_manifest};
+
+
+fn setup() {
+}
+
+test!(cargo_metadata_simple {
+    let p = project("foo")
+            .file("Cargo.toml", &basic_bin_manifest("foo"));
+
+    assert_that(p.cargo_process("metadata"), execs().with_stdout(r#"
+[[packages]]
+dependencies = []
+id = "foo 0.5.0 [..]"
+manifest_path = "[..]Cargo.toml"
+name = "foo"
+version = "0.5.0"
+
+[packages.features]
+
+[[packages.targets]]
+kind = ["bin"]
+name = "foo"
+src_path = "src[..]foo.rs"
+
+[root]
+name = "foo"
+version = "0.5.0"
+
+"#));
+});
+
+
+test!(cargo_metadata_simple_json {
+    let p = project("foo")
+            .file("Cargo.toml", &basic_bin_manifest("foo"));
+
+    assert_that(p.cargo_process("metadata").arg("-f").arg("json"), execs().with_stdout(r#"
+        {
+            "root": {
+                "name": "foo",
+                "version": "0.5.0",
+                "features": null
+            },
+            "packages": [
+                {
+                    "name": "foo",
+                    "version": "0.5.0",
+                    "id": "foo[..]",
+                    "source": null,
+                    "dependencies": [],
+                    "targets": [
+                        {
+                            "kind": [
+                                "bin"
+                            ],
+                            "name": "foo",
+                            "src_path": "src[..]foo.rs"
+                        }
+                    ],
+                    "features": {},
+                    "manifest_path": "[..]Cargo.toml"
+                }
+            ]
+        }"#.split_whitespace().collect::<String>()));
+});
+
+test!(cargo_metadata_with_invalid_manifest {
+    let p = project("foo")
+            .file("Cargo.toml", "");
+
+    assert_that(p.cargo_process("metadata"), execs().with_status(101)
+                                                    .with_stderr("\
+failed to parse manifest at `[..]`
+
+Caused by:
+  no `package` or `project` section found."))
+});
+
+test!(cargo_metadata_with_invalid_output_format {
+    let p = project("foo")
+            .file("Cargo.toml", &basic_bin_manifest("foo"));
+
+    assert_that(p.cargo_process("metadata").arg("--output-format").arg("XML"),
+                execs().with_status(101)
+                       .with_stderr("unknown format: XML, supported formats are TOML, JSON."))
+});
+
+test!(cargo_metadata_simple_file {
+    let p = project("foo")
+            .file("Cargo.toml", &basic_bin_manifest("foo"));
+
+    assert_that(p.cargo_process("metadata").arg("--output-path").arg("metadata.toml"),
+        execs().with_status(0));
+
+    let outputfile = p.root().join("metadata.toml");
+    assert_that(&outputfile, existing_file());
+
+    let mut output = String::new();
+    File::open(&outputfile).unwrap().read_to_string(&mut output).unwrap();
+
+    assert_that(output[..].contains(r#"name = "foo""#), is(equal_to(true)));
+});
index a435f91b860fc77614d91fd8e618e82adea3f193..43a97fb636b075b6f089f362fa00a252957c481a 100644 (file)
@@ -51,6 +51,7 @@ mod test_cargo_freshness;
 mod test_cargo_generate_lockfile;
 mod test_cargo_init;
 mod test_cargo_install;
+mod test_cargo_metadata;
 mod test_cargo_new;
 mod test_cargo_package;
 mod test_cargo_profiles;